Skip to content

Conversation

calebdw
Copy link
Contributor

@calebdw calebdw commented Aug 31, 2025

Hello!

Summary

This PR introduces extension interfaces that allow PHPStan extensions to dynamically specify the $this context for closure parameters.

Problem

Currently, PHPStan only supports static @param-closure-this PHPDoc annotations, which cannot handle dynamic scenarios where the closure's $this type depends on runtime context. This particularly affects frameworks like PestPHP, which heavily rely on closures with dynamic $this bindings, and cannot properly communicate type information to PHPStan.

Real-world Example (PestPHP)

// PestPHP test file
test('user can login', function () {
    // PHPStan thinks $this is PHPUnit\Framework\TestCase (from @param-closure-this)
    // But it's actually the user's custom test class with additional methods
    $this->loginAs($user);  // <- PHPStan error: method not found
    $this->assertAuthenticated();
});

Solution

This PR adds three new extension interfaces that mirror the existing parameter type extensions:

  • FunctionParameterClosureThisExtension - For function calls
  • MethodParameterClosureThisExtension - For method calls
  • StaticMethodParameterClosureThisExtension - For static method calls

These extensions can inspect the call context (AST nodes, scope, arguments) and return the appropriate $this type for the closure.

Implementation Details

Architecture

The implementation follows PHPStan's established patterns with minimal invasiveness:

  1. Extension interfaces define the API for specifying closure $this types
  2. Provider infrastructure manages and lazy-loads registered extensions
  3. Integration in NodeScopeResolver checks extensions when processing closures
  4. Service tags for DI registration: phpstan.functionParameterClosureThisExtension, etc.

Usage Example

Here's a psuedo-code example of how PestPHP could use this feature:

<?php
use PHPStan\Type\FunctionParameterClosureThisExtension;

class PestTestExtension implements FunctionParameterClosureThisExtension
{
    public function isFunctionSupported(FunctionReflection $function, ParameterReflection $parameter): bool
    {
        return in_array($function->getName(), ['test', 'it', 'beforeEach', 'afterEach'])
            && $parameter->getName() === 'closure';
    }

    public function getClosureThisTypeFromFunctionCall(FunctionReflection $function, FuncCall $call, ParameterReflection $parameter, Scope $scope): ?Type
    {
        // Detect the test class from file/namespace context
        $namespace = $scope->getNamespace();
        if ($namespace !== null && str_ends_with($namespace, '\\Tests')) {
            $testClass = $this->findTestClassInFile($scope->getFile());
            if ($testClass !== null) {
                return new ObjectType($testClass);
            }
        }

        return null;
    }
}

Register in phpstan.neon:

services:
  - class: PestTestExtension
    tags: [phpstan.functionParameterClosureThisExtension]

Thanks!

@calebdw calebdw force-pushed the calebdw/push-runsnlywzztp branch from a363cb6 to 422458b Compare August 31, 2025 04:21
@ondrejmirtes ondrejmirtes merged commit 70a039f into phpstan:2.1.x Sep 2, 2025
450 of 456 checks passed
@ondrejmirtes
Copy link
Member

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants